/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.utils.resource;

import cn.easyplatform.lang.Encoding;
import cn.easyplatform.lang.Lang;
import cn.easyplatform.lang.Stopwatch;
import cn.easyplatform.lang.Streams;
import cn.easyplatform.lang.util.ClassTools;
import cn.easyplatform.lang.util.Disks;
import cn.easyplatform.lang.util.FileVisitor;
import cn.easyplatform.utils.resource.impl.ErrorResourceLocation;
import cn.easyplatform.utils.resource.impl.FileResource;
import cn.easyplatform.utils.resource.impl.ResourceLocation;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * 资源扫描的帮助函数集
 *
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a>
 */
public class Scans {

    /**
     * 在App环境
     *
     * @param lib
     * @return
     */
    public Scans init(String lib) {
        Stopwatch sw = Stopwatch.begin();
        File[] jars = new File(lib).listFiles();
        if (jars != null) {
            for (File file : jars) {
                if (file.getName().endsWith(".jar"))
                    locations.add(ResourceLocation.jar(file.getPath()));
            }
        } else {
            try {
                registerLocation(new File(lib).toURI().toURL());
            } catch (MalformedURLException e) {
                if (log.isWarnEnabled())
                    log.warn(lib + " NOT found?!");
            }
        }
        sw.stop();
        printLocations(sw);
        return this;
    }

    /**
     * 在Web环境
     * <p/>
     */
    public Scans initWeb(String base) {
        Stopwatch sw = Stopwatch.begin();
        // 获取classes文件夹的路径
        String classesPath = base + "/classes/";
        if (classesPath != null) {
            locations.add(ResourceLocation.file(new File(classesPath)));
        } else {
            if (log.isWarnEnabled())
                log.warn("/WEB-INF/classes/ NOT found?!");
        }

        // 获取lib文件夹中的全部jar
        Collection<File> jars = FileUtils.listFiles(new File(base + "/lib"), new String[]{"jar"}, false);
        for (File jar : jars)
            locations.add(ResourceLocation.jar(jar.getPath()));
        sw.stop();
        printLocations(sw);
        return this;
    }

    private void printLocations(Stopwatch sw) {
        if (log.isDebugEnabled()) {
            log.debug("Locations count={} time use {}ms", locations.size(), sw.getDuration());
        }
        if (log.isTraceEnabled()) {
            StringBuilder sb = new StringBuilder();
            for (ResourceLocation rc : locations)
                sb.append('\t').append(rc.toString()).append("\r\n");
            log.trace("Locations for Scans:\n" + sb);
        }
    }

    public void registerLocation(Class<?> klass) {
        if (klass == null)
            return;
        try {
            registerLocation(klass.getProtectionDomain().getCodeSource()
                    .getLocation());
        } catch (Throwable e) {
            String classFile = klass.getName().replace('.', '/') + ".class";
            URL url = ClassTools.getClassLoader().getResource(classFile);
            if (url != null) { // 基本上不可能为null
                String str = url.toString();
                try {
                    str = URLDecoder.decode(str, Encoding.UTF8);
                } catch (UnsupportedEncodingException e1) {
                    throw Lang.impossible();
                }
                str = str.substring(0, str.length() - classFile.length());
                try {
                    registerLocation(new URL(str));
                } catch (Throwable e2) {
                    if (log.isInfoEnabled())
                        log.info("Fail to registerLocation --> " + str, e);
                }
            }
        }
    }

    public void registerLocation(URL url) {
        if (url == null)
            return;
        locations.add(makeResourceLocation(url));
    }

    protected ResourceLocation makeResourceLocation(URL url) {
        try {
            String str = url.toString();
            if (str.endsWith(".jar")) {
                return ResourceLocation.jar(str);
            } else if (str.contains("jar!")) {
                if (str.startsWith("jar:file:")) {
                    str = str.substring("jar:file:".length());
                }
                return ResourceLocation.jar(str.substring(0,
                        str.lastIndexOf("jar!") + 3));
            } else if (str.startsWith("file:")) {
                return ResourceLocation.file(new File(url.getFile()));
            } else {
                if (log.isDebugEnabled())
                    log.debug("Unkown URL " + url);
                // return ResourceLocation.file(new File(url.toURI()));
            }
        } catch (Throwable e) {
            if (log.isInfoEnabled())
                log.info("Fail to registerLocation --> " + url, e);
        }
        return new ErrorResourceLocation(url);
    }

    public List<GResource> scan(String src) {
        return scan(src, null);
    }

    /**
     * 在磁盘目录或者 CLASSPATH(包括 jar) 中搜索资源
     * <p/>
     * <b>核心方法</b>
     *
     * @param src   起始路径
     * @param regex 资源名需要匹配的正则表达式
     * @return 资源列表
     */
    public List<GResource> scan(String src, String regex) {
        List<GResource> list = new ArrayList<GResource>();
        Pattern pattern = regex == null ? null : Pattern.compile(regex);
        // 先看看是不是文件系统上一个具体的文件
        if (src.startsWith("~/"))
            src = Disks.normalize(src);
        File srcFile = new File(src);
        if (src.startsWith("/") || srcFile.exists()) {
            if (srcFile.exists()) {
                if (srcFile.isDirectory()) {
                    Disks.visitFile(srcFile,
                            new ResourceFileVisitor(list, src),
                            new ResourceFileFilter(pattern));
                } else {
                    list.add(new FileResource(src, srcFile));
                }
            } else
                scan(src.substring(1), regex);
            // 虽然已经找到一些了, 但还是扫描一些吧,这样才全!!
        }
        for (ResourceLocation location : locations) {
            location.scan(src, pattern, list);
        }
        // 如果啥都没找到,那么,用增强扫描
        if (list.isEmpty()) {
            try {
                Enumeration<URL> enu = ClassTools.getClassLoader()
                        .getResources(src);
                if (enu != null && enu.hasMoreElements()) {
                    while (enu.hasMoreElements()) {
                        try {
                            URL url = enu.nextElement();
                            ResourceLocation loc = makeResourceLocation(url);
                            if (url.toString().contains("jar!"))
                                loc.scan(src, pattern, list);
                            else
                                loc.scan("", pattern, list);
                        } catch (Throwable e) {
                            if (log.isTraceEnabled())
                                log.trace("", e);
                        }
                    }
                }
            } catch (Throwable e) {
                if (log.isDebugEnabled())
                    log.debug("Fail to run deep scan!", e);
            }
        }
        list = new ArrayList<GResource>((new HashSet<GResource>(list)));
        if (log.isDebugEnabled())
            log.debug("Found %s resource by src( {} ) , regex( {} )",
                    list.size(), src, regex);
        return list;
    }

    public List<Class<?>> scanPackage(Class<?> classZ) {
        return scanPackage(classZ.getPackage().getName(), FLT_CLASS);
    }

    public List<Class<?>> scanPackage(Class<?> classZ, String regex) {
        return scanPackage(classZ.getPackage().getName(), regex);
    }

    /**
     * 搜索并返回给定包下所有的类（递归）
     *
     * @param pkg 包名或者包路径
     */
    public List<Class<?>> scanPackage(String pkg) {
        return scanPackage(pkg, FLT_CLASS);
    }

    /**
     * 搜索给定包下所有的类（递归），并返回所有符合正则式描述的类
     *
     * @param pkg   包名或者包路径
     * @param regex 正则表达式，请注意你需要匹配的名称为 'xxxx.class' 而不仅仅是类名，从而保证选出的对象都是类文件
     */
    public List<Class<?>> scanPackage(String pkg, String regex) {
        String packagePath = pkg.replace('.', '/').replace('\\', '/');
        if (!packagePath.endsWith("/"))
            packagePath += "/";
        return rs2class(pkg, scan(packagePath, regex));
    }

    public static boolean isInJar(File file) {
        return isInJar(file.getAbsolutePath());
    }

    public static boolean isInJar(String filePath) {
        return filePath.contains(".jar!");
    }

    public static GResource makeJarGResource(File file) {
        return makeJarGResource(file.getAbsolutePath());
    }

    public static GResource makeJarGResource(String filePath) {
        JarEntryInfo jeInfo = new JarEntryInfo(filePath);
        try {
            ZipInputStream zis = makeZipInputStream(jeInfo.getJarPath());
            ZipEntry ens = null;
            while (null != (ens = zis.getNextEntry())) {
                if (ens.isDirectory())
                    continue;
                if (jeInfo.getEntryName().equals(ens.getName())) {
                    return makeJarGResource(jeInfo.getJarPath(), ens.getName(),
                            "");
                }
            }
        } catch (IOException e) {
        }
        return null;
    }

    public static GResource makeJarGResource(final String jarPath,
                                             final String entryName, final String base) throws IOException {
        GResource gResource = new GResource() {

            public InputStream getInputStream() {
                try {
                    ZipInputStream zis = makeZipInputStream(jarPath);
                    ZipEntry ens = null;
                    while (null != (ens = zis.getNextEntry())) {
                        if (ens.getName().equals(entryName))
                            return zis;
                    }
                } catch (IOException ex) {
                    throw Lang.wrapThrow(ex);
                }
                throw Lang.impossible();
            }

            public int hashCode() {
                return (jarPath + ":" + entryName).hashCode();
            }
        };
        if (entryName.equals(base))
            gResource.setName(entryName);
        else
            gResource.setName(entryName.substring(base.length()));
        return gResource;
    }

    public static ZipInputStream makeZipInputStream(String jarPath)
            throws MalformedURLException, IOException {
        ZipInputStream zis = null;
        try {
            zis = new ZipInputStream(new FileInputStream(jarPath));
        } catch (IOException e) {
            zis = new ZipInputStream(new URL(jarPath).openStream());
        }
        return zis;
    }

    public static final Scans me() {
        return me;
    }

    /**
     * 将一组 GResource 转换成 class 对象
     * <p>
     * 包前缀
     *
     * @param list 列表
     * @return 类对象列表
     */
    private static List<Class<?>> rs2class(String pkg, List<GResource> list) {
        Set<Class<?>> re = new HashSet<Class<?>>(list.size());
        if (!list.isEmpty()) {
            for (GResource nr : list) {
                if (!nr.getName().endsWith(".class")) {
                    continue;
                }
                // Class快速载入
                String className = pkg
                        + "."
                        + nr.getName().substring(0, nr.getName().length() - 6)
                        .replaceAll("[/\\\\]", ".");
                try {
                    Class<?> klass = Lang.loadClass(className);
                    re.add(klass);
                    continue;
                } catch (Throwable e) {
                }
                // 失败了? 尝试终极方法,当然了,慢多了
                InputStream in = null;
                try {
                    in = nr.getInputStream();
                    className = ClassTools.getClassName(in);
                    if (className == null) {
                        if (log.isInfoEnabled())
                            log.info(
                                    "Resource can't map to Class, Resource {}",
                                    nr);
                        continue;
                    }
                    Class<?> klass = Lang.loadClass(className);
                    re.add(klass);
                } catch (ClassNotFoundException e) {
                    if (log.isInfoEnabled())
                        log.info("Resource can't map to Class, Resource {}",
                                nr, e);
                } finally {
                    Streams.safeClose(in);
                }
            }
        }
        return new ArrayList<Class<?>>(re);
    }

    public static class ResourceFileFilter implements FileFilter {
        public boolean accept(File f) {
            if (f.isDirectory()) {
                String fnm = f.getName().toLowerCase();
                // 忽略 SVN 和 CVS 文件,还有Git文件
                if (".svn".equals(fnm) || ".cvs".equals(fnm)
                        || ".git".equals(fnm))
                    return false;
                return true;
            }
            if (f.isHidden())
                return false;
            return pattern == null || pattern.matcher(f.getName()).find();
        }

        private Pattern pattern;

        public ResourceFileFilter(Pattern pattern) {
            super();
            this.pattern = pattern;
        }
    }

    public static class ResourceFileVisitor implements FileVisitor {
        public void visit(File f) {
            list.add(new FileResource(base, f));
        }

        String base;
        List<GResource> list;

        public ResourceFileVisitor(List<GResource> list, String base) {
            super();
            this.list = list;
            this.base = base;
        }
    }

    private static final String FLT_CLASS = "^.+[.]class$";

    private static final Logger log = LoggerFactory.getLogger(Scans.class);

    private static final Scans me = new Scans();

    private Set<ResourceLocation> locations = new HashSet<ResourceLocation>();

    private Scans() {
        // 当前文件夹
        //String file = System.getProperty("easyplatform.home");
        //if (file == null)
        //    file = System.getProperty("user.dir");
        //locations.add(ResourceLocation.file(new File(file)));
        // 推测一下easyplatform自身所在的位置
        registerLocation(Scans.class);
    }
}
